package de.mohlendo.brainfuck
{
  
  
  
  /**
   *  Dispatched when the user clicks on an item in the control.
   *
   *  @eventType com.coremedia.flex.components.events.TagCloudEvent.ITEM_CLICK
   */
  [Event(name="breakpoint", type="brainfuck.BreakpointEvent")]
  
  [Event(name="position", type="brainfuck.PositionEvent")]
  
  public class Brainfuck
  {
    private var _data:Array = [0];
    
    private var bracketOpen:Array = new Array(); //Stack, that stores the position of the open brackets
    
    private var _ptr:uint = 0;
    
    private var index:int;
    
    private var _source:String;
    
    protected var _output:String = "";
    
    private var _inputCallback:Function;
    
    private var _debugEnabled:Boolean = false;
    
    private var _running:Boolean = false;
    
    
    public function get running():Boolean {
      return _running;
    }
    
    public function set debugEnabled(value:Boolean):void {
      this._debugEnabled = value;
    }
    
    public function get debugEnabled():Boolean {
      return _debugEnabled;
    }
    
    public function set inputCallback(value:Function):void {
     this._inputCallback = value
   }
    
    public function get source():String {
     return _source;
   } 
   
   [Bindable]
   public function set source(value:String):void {
     this._source = value
   }

   public function get data():Array {
     return _data;
   }
   
   public function get output():String {
     return this._output;
   }
   
   /**
   * Runs the program in source. If debuging is enabled, it just runs to the next
   * breakpoint
   * */
   public function run():void {
     this._running = true;
     while (index < _source.length) {
       var breakP:Boolean = next_token();
       if(breakP && _debugEnabled) {
         return;
       }
     }
     if(index >= _source.length) {
       this._running = false;
     }
   }
   
   /**
   * Steps through the program
   * */
   public function nextStep():void {
     if (index < _source.length) {
       next_token();       
     }
   }
   
   protected function notify(message:String):void {
      throw new Error(message + " near " + index);
    }
    
   private function print():void {
     var out:String = String.fromCharCode(_data[_ptr]);
     trace(out); 
     this._output+=out;
   }
   
   private function next_token():Boolean {     
    var ch:String = next();
    switch (ch) {
      case '>': //increment the pointer (to point to the next cell to the right).
        ++_ptr;
        if(_data[_ptr] == undefined) {
          _data[_ptr] = 0;
          this.dispatchEvent(new PositionEvent("position",_ptr,PositionEvent.RIGHT));
        }        
        break;
      case '<': //decrement the pointer (to point to the next cell to the left).
        if(_ptr==0) {
          notify("Buffer underflow");          
        }
        --_ptr;
        this.dispatchEvent(new PositionEvent("position",_ptr,PositionEvent.LEFT));
        break;
      case '+': //increment (increase by one) the byte at the pointer.
        if(_data[_ptr] < 255) {
          ++_data[_ptr];
        } else {
          notify("Buffer overflow");
        }
        break;
      case '-': //  decrement (decrease by one) the byte at the pointer.
        if(_data[_ptr] > 0) {
          --_data[_ptr];
        } else {
          notify("Buffer underflow");
        }
        break;
      case '.': //  output the value of the byte at the pointer.
        print();
        break;
      case ',': // accept one byte of input, storing its value in the byte at the pointer.
        var input:uint = _inputCallback.call();
        _data[_ptr]=input;
        break;
      case '[': //  jump forward to the command after the corresponding ] if the byte at the pointer is zero.
        if(_data[_ptr] == 0) {
          var brackets:int = 0;
          while(true) {
            index++;
            if(index > _source.length) {
              break;
            }
            if (_source.charAt(index) == '[') {
              brackets++;
            } else if (_source.charAt(index) == ']') {
              brackets--;
              if (brackets==-1)
              {
                break;
              }
            }
          }
        }
        bracketOpen.push(index);
        break;
      case ']': //  jump back to the command after the corresponding [ if the byte at the pointer is nonzero.
        if(_data[_ptr] > 0) {
          index = bracketOpen.pop()-1;
        }
        break;
      case '#': // debugging brakepoint
        if(_debugEnabled) {
          this.dispatchEvent(new BreakpointEvent("breakpoint",index));
          return true;
          break;
        }        
      default:
        break;
    }
    return false;
   
   }  
   
   /**
   * Returns the next character from the source
   */
  private function next():String {
    var ch:String = _source.charAt(index++);   
    return ch;
  }
    
  }
}